Skip to content

Add a fine-grained text splice op#346

Open
johannesmutter wants to merge 1 commit into
op-engine/1-commit-funnelfrom
op-engine/2-splice-ops
Open

Add a fine-grained text splice op#346
johannesmutter wants to merge 1 commit into
op-engine/1-commit-funnelfrom
op-engine/2-splice-ops

Conversation

@johannesmutter

Copy link
Copy Markdown
Collaborator

Part 2/4 of the op-engine series (#335). Stacked on op-engine/1-commit-funnel.

  • Transaction.splice(path, start, delete_count, text) records ['splice', [node_id, prop], start, delete_count, text] instead of a whole-value copy per keystroke; insert_text/delete_selection emit splices with unchanged public behavior
  • mark/annotation range adjustment lives in splice_annotated_text (doc_utils), the single source of truth for forward and inverse ops; collapsed range nodes are cascade-GC'd like set() does
  • pure insertions (every keystroke) take a fast path with no value copies and no round-trip check; lossy deletions carry the prior ranges verbatim on the inverse, so undo is always exact
  • history batching coalesces contiguous typing splices into one op using the inverses' stored lengths, so a one-second typing batch stores one small op instead of a full string copy per keystroke
  • offsets are grapheme clusters (Intl.Segmenter), the same unit as all existing svedit text offsets

Open question for your review (RFC question 2 in #335): the inverse currently has two shapes — a plain 5-element splice when lossless, a 6th prior-ranges element when lossy. The alternative is one shape that always carries prior ranges. Happy to change based on your take.

Tests: 10 for this PR.

🤖 Generated with Claude Code

Text edits previously recorded ['set', path, whole_value] twice per
keystroke (ops + inverse_ops), so history stored a full copy of the
paragraph per keystroke and the op stream carried no meaning beyond
"content changed".

Transaction.splice(path, start, delete_count, text) records
['splice', [node_id, prop], start, delete_count, text] instead.
Mark/annotation range adjustment happens inside op application
(splice_annotated_text in doc_utils.ts, the single source of truth used
by forward and inverse ops alike), and range nodes whose range collapsed
away are garbage-collected ref-count-aware, like set() does.

Pure insertions (every keystroke) take a fast path: insertion
adjustment can never remove or truncate ranges, so the inverse is a
plain splice by construction and no value copies or round-trip checks
run on the hot path. Deletions verify losslessness with a round-trip
check; lossy deletions carry the prior ranges verbatim as a 6th op
element, so undo is always exact.

insert_text and delete_selection (text branch) now emit splices; their
public behavior is unchanged, and plain typing returns early before the
mark/annotation restoration copies. History batching coalesces
consecutive contiguous typing splices into one op using the inverses'
stored lengths (no re-segmentation of the accumulated batch text), so a
one-second typing batch stores a single small op instead of one string
copy per keystroke.

Character offsets are grapheme clusters (Intl.Segmenter), the same unit
as all existing svedit text offsets.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
svedit Ready Ready Preview, Comment Jul 18, 2026 9:16am

@michael
michael marked this pull request as draft July 22, 2026 10:29
@michael
michael marked this pull request as ready for review July 22, 2026 10:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant